package com.hoki.zj.hrm.service.impl;

import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.mapper.Wrapper;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import com.hoki.zj.hrm.asserts.GlobalAssert;
import com.hoki.zj.hrm.domain.KillCourse;
import com.hoki.zj.hrm.domain.KillOrder;
import com.hoki.zj.hrm.domain.KillOrderItem;
import com.hoki.zj.hrm.dto.MyOrderDto;
import com.hoki.zj.hrm.mapper.KillCourseMapper;
import com.hoki.zj.hrm.service.IKillCourseService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.time.DateUtils;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

import static com.hoki.zj.hrm.constant.MQMessageConst.*;
import static com.hoki.zj.hrm.domain.KillCourse.*;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author shukei
 * @since 2021-09-29
 */
@Service
@Slf4j
public class KillCourseServiceImpl extends ServiceImpl<KillCourseMapper, KillCourse> implements IKillCourseService {

    /** 秒杀课程发布key */
    public static final String REDIS_PUBLISH_KEY = "KILL_COURSE_KILL";
    /** 秒杀课程库存key前缀 */
    public static final String PRE_NAME_IN_REDISSON_SEMAPHORE_OF_KILL_COUNT = "KILL_COURSE_STOCK:";
    /** 秒杀课程记录key前缀 */
    public static final String PRE_REDIS_KILL_LOG_KEY = "KILL_LOG:";

    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

    @Autowired
    private RedissonClient redissonClient ;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 1.秒杀添加(保存到数据库)
     * @param killCourse killCourse: courseId, killCount, startTime, endTime, killPrice, courseName, coursePic
     */
    @Override
    public void addKillCourse2DB(KillCourse killCourse) {
        // 1.判断参数
        GlobalAssert.isNull(killCourse.getCourseId(), "无效的秒杀课程!");
        // 2.保存到数据库
        // 2.1设置killCourse
        setKillCourse(killCourse);
        // 2.2保存
        insert(killCourse);
    }

    // 设置killCourse
    private void setKillCourse(KillCourse killCourse) {
        String sessionNumber = UUID.randomUUID().toString(); // 获取场次编号
        killCourse.setSessionNumber(sessionNumber); // 设置场次编号
        killCourse.setKillLimit(1); // 设置可秒杀的数量
        killCourse.setKillStatus(KILL_STATUS_UNRELEASED); // 设置秒杀状态,未发布
        killCourse.setCreateTime(new Date()); // 添加时间
    }

    /**
     * 2.定时发布到Redis（修改数据库的状态）
     */
    @Override
    public void addKillCoursePublishTask() {
        // 1.查询数据库所有未发布的秒杀课程
        Wrapper<KillCourse> wrapper = new EntityWrapper<>();
        // 1.1查询条件1:状态为未发布
        wrapper.eq("kill_status", KILL_STATUS_UNRELEASED);
        // 1.2查询条件2:时间在当前(正式上线23点定时任务)时间的两天内
        Date startTime = new Date();
        Date endTime = DateUtils.addDays(startTime, 2);
        wrapper.between("start_time", startTime, endTime);
        // 1.3查询符合条件的秒杀课程集合
        List<KillCourse> killCourses = selectList(wrapper);
        // 2.查询结果不为空才执行发布流程
        if (!killCourses.isEmpty()) {
            for (KillCourse killCourse : killCourses) {
                /*
                    1.发布秒杀课程到Redis
                    1.1 hash:
                         key   :  KILL_COURSE,
                         hkey  :  CourseId,
                         hvalue:  killCourse
                    1.2并完成幂等处理   putIfAbsent() 相当于 setnx
                 */
                Boolean putSuccess = redisTemplate.opsForHash().
                        putIfAbsent(REDIS_PUBLISH_KEY, killCourse.getId().toString(), killCourse);
                // 1.3 保存失败,结束当前次循环
                if (!putSuccess) {
                    log.warn("秒杀课程发布到Redis异常!");
                    continue;
                }
                // 1.4.保存库存到Redis(信号量)
                // 获取库存
                Integer killCount = killCourse.getKillCount();
                // 获取当前秒杀课程的信号量
                RSemaphore semaphore = redissonClient.
                        getSemaphore(PRE_NAME_IN_REDISSON_SEMAPHORE_OF_KILL_COUNT
                                + killCourse.getId().toString());
                // 设置库存为信号量
                boolean setSuccess = semaphore.trySetPermits(killCount);
                if (!setSuccess) {
                    // 设置失败,回滚数据(删除发布到Redis的秒杀课程)
                    redisTemplate.opsForHash().delete(REDIS_PUBLISH_KEY, killCourse.getId().toString(), killCourse);
                    continue;
                }
                // 1.5 保存成功.数据库修改状态
                killCourse.setKillStatus(KILL_STATUS_RELEASED);
                updateById(killCourse);
                log.info("秒杀课程发布成功!");
            }
        }
    }

    /**
     * 3.定时任务:秒杀课程清除过期的课程
     */
    @Override
    public void cleanKillCourseExpiredTask() {
        // 1.获取Redis中发布的课程
        BoundHashOperations<Object, String, KillCourse> ops =
                redisTemplate.boundHashOps(REDIS_PUBLISH_KEY);
        List<KillCourse> killCoursesInRedis = ops.values();
        // 批量删除课程id
        Date now = new Date();
        killCoursesInRedis.forEach(killCourse -> {
            if (now.after(killCourse.getEndTime())) {
                // 过期就删除对应的库存
                RSemaphore semaphore = redissonClient.getSemaphore(PRE_NAME_IN_REDISSON_SEMAPHORE_OF_KILL_COUNT
                        + killCourse.getId().toString());
                boolean delete = semaphore.delete();
                if (delete) {
                    // 删除库存成功,删除对应的课程
                    ops.delete(killCourse.getId().toString());
                    // 修改数据库状态
                    killCourse.setKillStatus(KILL_STATUS_KILL_END);
                    updateById(killCourse);
                    log.info("已成功清除过期课程!");
                }
            }
        });
    }

    /**
     * 4.秒杀课程展示
     * @return
     */
    @Override
    public List<KillCourse> getAllKillCourses() {
        // 1.获取Redis中发布的课程
        BoundHashOperations<Object, String, KillCourse> ops = redisTemplate.boundHashOps(REDIS_PUBLISH_KEY);
        List<KillCourse> killCourses = ops.values();
        if (killCourses.isEmpty()) return null; // 未发布任何课程返回null
        Date now = new Date();
        // 2.过滤发布课程中所有未过期的课程并返回
        List<KillCourse> availableKillCourse = killCourses.stream()
                .filter(killCourse -> killCourse.getEndTime().after(now))
                .collect(Collectors.toList());
        if (availableKillCourse.isEmpty()) return null; // 所有课程均已过期,返回null
        return availableKillCourse; // 返回所有未过期的课程
    }

    /**
     * 5.秒杀课程单条查询
     * @param id
     * @return
     */
    @Override
    public KillCourse getOneKillCourse(Long id) {
        // 1.判断参数
        GlobalAssert.isNull(id, "无效的课程id");
        // 2.获取Redis中发布的课程
        BoundHashOperations<Object, String, KillCourse> ops = redisTemplate.boundHashOps(REDIS_PUBLISH_KEY);
        List<KillCourse> killCourses = ops.values();
        if (killCourses.isEmpty()) return null; // 未发布任何课程返回null
        Date now = new Date();
        // 3.过滤当前id,并且未过期的课程
        List<KillCourse> availableKillCourse = killCourses.stream()
                .filter(killCourse -> killCourse.getId().equals(id) && killCourse.getEndTime().after(now))
                .collect(Collectors.toList());
        if (availableKillCourse.isEmpty()) return null; // 查询的课程已过期,返回null
        // 4.返回
        return availableKillCourse.get(0);
    }

    /**
     * 6.秒杀课程执行,异步下单
     * @param id killCourseId
     */
    @Override
    public String executeKill(Long id) {
        // 1.判断参数
        GlobalAssert.isNull(id, "无效的课程id");
        // 2.登录判断
//        LoginUser loginUser = LoginUserContext.getLoginUser();
//        String loginUserStr = JSON.toJSONString(loginUser);
//        GlobalAssert.hasText(loginUserStr, "请登录!");
        Long userId = 3L;
        // 3.判断当前商品是否合法
        KillCourse killCourse = getOneKillCourse(id);
        GlobalAssert.isNull(killCourse, "无效的课程,或以超时");
        log.info("有效课程 -> {}", killCourse);
        // 3.1 判断是否已经秒杀过
        Boolean isKilled = redisTemplate.hasKey(PRE_REDIS_KILL_LOG_KEY + ":" + userId + ":" + killCourse.getId());
        GlobalAssert.isUnsuccess(isKilled,"您已经秒杀过了");
        // 4.Redis减库存
        RSemaphore semaphore = redissonClient.getSemaphore(PRE_NAME_IN_REDISSON_SEMAPHORE_OF_KILL_COUNT
                + killCourse.getId().toString());
        boolean acquireSuccess = semaphore.tryAcquire(1); // 减去库存1
        // 4.1 秒杀失败
        GlobalAssert.isSuccess(acquireSuccess, "当前商品已秒杀完毕,请等待补货");
        // 5.下单
        String orderNo = UUID.randomUUID().toString(); // 创建订单号
        MyOrderDto myOrderDto = new MyOrderDto();
        // 订单
        placeAnOrder(userId, killCourse, orderNo, myOrderDto);
        rabbitTemplate.convertAndSend
                (
                    EXCHANGE_NAME_ORDER,
                    "killcourse.order",
                    myOrderDto
                );
        // 6.存放秒杀记录到Redis
        redisTemplate.opsForValue().set(PRE_REDIS_KILL_LOG_KEY + userId,
                killCourse);
        // 7.返回订单号
        return orderNo;
    }

    // 生成订单,异步下单对象
    private void placeAnOrder(Long userId, KillCourse killCourse, String orderNo, MyOrderDto myOrderDto) {
        BigDecimal killPrice = killCourse.getKillPrice();
        Long killCourseId = killCourse.getId();
        String courseName = killCourse.getCourseName();
        // 订单
        KillOrder killOrder = new KillOrder();
        killOrder.setOrderNo(orderNo); // 订单号
        killOrder.setTotalPrice(killPrice); // 价格
        killOrder.setUserId(userId); // 购买用户id
        killOrder.setKillCount(1); // 数量
        // 订单详情
        KillOrderItem killOrderItem = new KillOrderItem();
        killOrderItem.setKillCourseId(killCourseId); // 秒杀程课id
        killOrderItem.setKillCourseName(courseName); // 课程名字
        // 设置异步下单消息内容
        myOrderDto.setKillOrder(killOrder);
        myOrderDto.setKillOrderItem(killOrderItem);
    }

}
